#!/usr/bin/env python

"""
PyTDMaker is a decription and torrent generator for uploads to PTP. Use this program
to generate MediaInfo, screenshots, and a torrent file for upload.

PyTDMaker is meant as a successor to TDMaker, a C# program that provided similar
functionality. However, TDMaker has since become very buggy and the output no longer
matches PTP's required upload format. 

Features:
- Cross-platform support
- Generates MediaInfo, screenshots, and a torrent file
- Uploads screenshots to ptpimg.me
- Threaded processing

PyTDMaker is free software: you can redistribute it and/or modify it under the terms
of the GNU General Public License as published by the Free Software Foundation, either
version 3 of the License, or (at your option) any later version.

PyTDMaker is distributed in the hope that will be useful, but WITHOUT ANY WARRANTY; 
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License  along with PyTDMaker.
If not, see <http://www.gnu.org/licenses/>.
"""

import os
import os.path
import sys
import re
import math
import string
import httplib
import time
import copy
import functools
import logging
import mimetypes
import hashlib
import subprocess
import inspect
import operator
import tempfile
import argparse
import platform
import Queue
import StringIO
import random
import Tkinter
import tkFileDialog
import tkMessageBox

try:
    import threading
except ImportError:
    import dummy_threading as threading

MKTORRENT_RATIO = 0.1875
NUM_PICS = 3
MAX_FD_OPEN = 50

try:
    WindowsError
except NameError:
    class WindowsError(Exception):
        pass

def subprocess_attempt(command, tries=3):
    i = 0
    logging.debug("Executing command: {0}".format(command))
    try:
        output = subprocess.check_output(command)
    except subprocess.CalledProcessError as inst:
        logging.warning("Command returned with an error on try {1}. {0}".format(inst, i))
        if i >= tries:
            logging.error("Command failed to execute.")
            raise inst
    else:
        logging.debug("Command executed successfully.")
        return output
    finally:
        i += 1

class Torrent:
    def __init__(self, announce, i=18, private=True):
        self.piece_len = 2 ** i
        self.meta = {
            "announce": str(announce),
            "creation date": int(time.time()),
            "encoding": "utf-8",
            "info": {
                "piece length": self.piece_len,
                "pieces": "",
                "private": 1 if private else 0,
            }
        }
        
        self.queue = Queue.Queue()
        self.fd = 0
        
        self._encode_funcs = {
            dict: self._encode_dict,
            list: self._encode_list,
            int: self._encode_int,
            long: self._encode_int,
            str: self._encode_str,
            unicode: self._encode_str
        }

    def file(self, filename):
        self.meta['info']['name'] = os.path.basename(filename)
        self.meta['info']['length'] = os.path.getsize(filename)

        hash = hashlib.md5()
        with open(filename, 'rb') as fp:
            contents = fp.read(128)
            hash.update(contents)
        self.meta['info']['md5sum'] = hash.hexdigest()
        
        i = 0
        for index in range(0, self.meta['info']['length'], self.piece_len):
            self.queue.put((i, filename, index))
            i += 1
        
        self.meta['info']['pieces'] = [u""] * int(math.ceil(float(self.meta['info']['length']) / self.piece_len))
    
    def directory(self, filename):
        files = []
        length = 0
        i = 0
        self.meta['info']['name'] = os.path.basename(filename)
        for root, dirs, files in os.walk(filename):
            for file in files:
                info = {
                    "length": os.path.getsize(filename),
                    "md5sum": "",
                    "path": os.path.relpath(file, filename)
                }
                length += int(math.ceil(float(info['length']) / self.piece_len))
                
                hash = hashlib.md5()
                with open(file, 'rb') as fp:
                    contents = fp.read(128)
                    hash.update(contents)
                info['md5sum'] = hash.hexdigest()
                
                for index in range(0, info['length'], self.piece_len):
                    self.queue.put((i, filename, index))
                    i += 1
                files.append(info)
        self.meta['info']['files'] = files
        self.meta['info']['pieces'] = [u""] * length
    
    def generate_torrent(self, num_threads=2):
        threads = []
        for i in range(num_threads):
            t = threading.Thread(target=self._hashing_thread)
            t.daemon = True
            threads.append(t)
        map(operator.methodcaller('start'), threads)
        self.queue.join()
        map(operator.methodcaller('join'), threads)
        
        retval = copy.deepcopy(self.meta)
        retval['info']['pieces'] = ''.join(retval['info']['pieces'])
        return self._encode(retval)
    
    def _hashing_thread(self):
        while not self.queue.empty():
            if self.fd >= MAX_FD_OPEN:
                time.sleep(1)
                continue
            self.fd += 1
            i, filename, index = self.queue.get(True)
            hash = hashlib.sha1()
            with open(filename, 'rb') as fp:
                fp.seek(index)
                hash.update(fp.read(self.piece_len))
            self.fd -= 1
            self.meta['info']['pieces'][i] = hash.digest()[:20]
            self.queue.task_done()
    
    def _encode(self, data):
        return self._encode_funcs[type(data)](data)
    
    def _encode_dict(self, data):
        values = [self._encode(str(key)) + self._encode(value) for key, value in data.iteritems()]
        return "d{0}e".format(''.join(values))
    
    def _encode_list(self, data):
        values = map(self._encode, data)
        return "d{0}e".format(''.join(values))
    
    def _encode_str(self, data):
        return "{0}:{1}".format(len(data), data)
    
    def _encode_int(self, data):
        return "i{0}e".format(data)

class VideoFile:
    def __init__(self, master, gui, filename):
        if platform.system() == "Windows":
            self.subprocess_prefix = []
            self.null = open("NUL", "w")
            path = os.path.dirname(inspect.getfile(inspect.currentframe()))
            if sys.maxsize > 2 ** 32:
                logging.info("Platform is win64. Using included binaries.")
                self.mediainfo_cmd = [os.path.join(path, "win64", "mediainfo")]
                self.ffmpeg_cmd = [os.path.join(path, "win64", "ffmpeg")]
            else:
                logging.info("Platform is win32. Using included binaries.")
                self.mediainfo_cmd = [os.path.join(path, "win32", "mediainfo")]
                self.ffmpeg_cmd = [os.path.join(path, "win32", "ffmpeg")]
        else:
            logging.info("Platform is Linux. Assuming dependencies already satisfied.")
            self.mediainfo_cmd = ["/usr/bin/env", "mediainfo"]
            self.ffmpeg_cmd = ["/usr/bin/env", "ffmpeg"]
            self.null = open("/dev/null", "w")
    
        self.filename = filename
        self.num_pics = master.num_pics
        self.gui = gui
        self.framerate = False
        self.duration = False
        self.mediainfo = ""
        self.screenshots = []
    
    def __str__(self):
        info = self.mediainfo
        for url in self.screenshots:
            info += "[img]{0}[/img]\n".format(url)
        return info
    
    def make_mediainfo(self):
        self.gui.put_info("Getting MediaInfo for {0}.\n".format(os.path.basename(self.filename)))
        mediainfo = self._get_mediainfo(self.filename, self.null, self.mediainfo_cmd)
        if mediainfo == False:
            self.gui.put_info('Error: Could not retrieve MediaInfo.', True)
            return False
        
        workfile = StringIO.StringIO(mediainfo)
        line = 'x'
        framerate = duration = False
        while line != '':
            line = workfile.readline()
            if 'Frame rate' in line and 'mode' not in line:
                framerate = float(line[line.find(':') + 2:line.rfind('fps') - 1])
                logging.info("Framerate is {0} for {1}.".format(framerate, self.filename))
            elif 'Duration' in line:
                index = line.find(":") + 1
                duration = 0
                if line.find('h', index) != -1:
                    # Find the hours if applicable
                    duration += int(line[index:line.find('h', index)]) * 3600
                    index = line.find('h') + 2
                if line.find('mn', index) != -1:
                    # Find the minutes if applicable
                    duration += int(line[index:line.find('mn')]) * 60
                    index = line.find('mn') + 3
                elif line.find('s') != -1: 
                    # Find the seconds if applicable
                    duration += int(line[index:line.find('s')])
                
                if duration == 0:
                    # Invalid duration line.
                    logging.warning("Duration line in MediaInfo was invalid.")
                    continue
                logging.info("Duration is {0} for {1}.".format(duration, self.filename))
        workfile.close()

        if framerate == False or duration == False:
            logging.error("Could not find duration/framerate in MediaInfo:\n{0}".format(mediainfo))
            self.gui.put_info('Error: Could not get duration/framerate from MediaInfo.', True)
            return False
        
        self.framerate = framerate
        self.duration = duration
        self.mediainfo = mediainfo
        return mediainfo
    
    def make_screenshots(self):
        if self.duration == False:
            logging.error("Tried to take screenshots before mediainfo.")
            self.gui.put_info("Tried to take screenshots before mediainfo. Please report to developers.", True)
            return False

        logging.info("Taking screenshots for {0}".format(self.filename))
        start = int(self.duration / 12)
        stop = int(11 * self.duration / 12)
        offsets = sorted([random.randint(start, stop) for i in range(self.num_pics)])
        logging.debug("Using following offsets: {0}".format(" ".join(map(str, offsets))))
        
        for offset in offsets:
            self.gui.put_info("Taking screenshot for {0} at {1}.\n".format(os.path.basename(self.filename), offset))
            filename = self._screenshot_take(self.filename, offset, self.null, self.ffmpeg_cmd)
            if filename == False:
                self.gui.put_info('Error: Could not take screenshots.', True)
                return False
            self.gui.put_info("Uploading screenshot for {0} at {1}.\n".format(os.path.basename(self.filename), offset))
            self.screenshots.append(self._screenshot_upload(filename))
        
        if not all(self.screenshots):
            return False
        return True
    
    def make_torrent(self, announce, torrentfile, threads=2):
        torrent = Torrent(announce)
        if os.path.isdir(self.filename):
            torrent.directory(self.filename)
        else:
            torrent.file(self.filename)
        self.gui.put_info("Generating torrent file...\n")
        contents = torrent.generate_torrent(threads)
        with open(torrentfile, 'wb') as fp:
            fp.write(contents)
        self.gui.put_info("Finished making torrent file.\n")
        return True
    
    @staticmethod
    def _get_mediainfo(filename, null, subprocess_prefix):
        logging.info("Getting MediaInfo for {0}".format(filename))
        mediainfo_params = [filename]
        try:
            mediainfo = subprocess_attempt(subprocess_prefix + mediainfo_params)
        except subprocess.CalledProcessError as inst:
            return False

        mediainfo = mediainfo.replace("\r\n", "\n")
        return mediainfo
        
    @staticmethod
    def _screenshot_take(filename, offset, null, subprocess_prefix):
        snapfile = "snapshot-{0}.png".format(offset)
        ffmpeg_params = ["-ss", str(offset), "-i", filename, "-y", "-an", "-sameq", "-f", "image2", "-frames", "1", snapfile]
        try:
            output = subprocess_attempt(subprocess_prefix + ffmpeg_params)
        except subprocess.CalledProcessError as inst:
            return False
        if not os.path.exists(snapfile):
            logging.error("Screenshot was supposedly taken but disappeared.")
            return False
        return snapfile
        
    @staticmethod
    def _screenshot_upload(filename):
        logging.debug("Uploading screenshot {0}.".format(filename))
        files = [('uploadfile', filename, open(filename, 'rb').read())]
        content_type, body = VideoFile._encode_multipart_formdata(files)
        conn = httplib.HTTPConnection('ptpimg.me')
        headers = { "Content-type": content_type, "Content-length": str(len(body)), "Referrer": "http://ptpimg.me" }
        conn.request('POST', '/index.php?type=uploadv3&key=QT5LGz7ktGFVZpfFArVHCpEvDcC3qrUZrf0kP', body, headers)
        data = eval(conn.getresponse().read())[0]
        os.unlink(filename)
        if "code" not in data.keys() or "ext" not in data.keys():
            logging.error("File upload failed.")
            return False
        logging.debug("File uploaded successfully to URL http://ptpimg.me/{0}.{1}.".format(data["code"], data["ext"]))
        return "http://ptpimg.me/{0}.{1}".format(data["code"], data["ext"])
    
    @staticmethod
    def _encode_multipart_formdata(files):
        LIMIT = '----------lImIt_of_THE_fIle_eW_$'
        CRLF = '\r\n'
        L = []
        for (key, filename, value) in files:
            L.append('--' + LIMIT)
            L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename))
            L.append('Content-Type: {0}'.format(mimetypes.guess_type(filename)[0] or 'application/octet-stream'))
            L.append('')
            L.append(value)
        L.append('--' + LIMIT + '--')
        L.append('')
        body = CRLF.join(L)
        content_type = 'multipart/form-data; boundary=%s' % LIMIT
        return content_type, body

class ThreadedText(Tkinter.Text):
    def __init__(self, master, **kwargs):
        Tkinter.Text.__init__(self, master, **kwargs)
        self.queue = Queue.Queue()
        self.config(state=Tkinter.DISABLED)
        master.after(1000, self.update)
    
    def write(self, text):
        self.queue.put(text)
    
    def update(self):
        while not self.queue.empty():
            try:
                line = self.queue.get_nowait()
            except Queue.Empty:
                break
            self.config(state=Tkinter.NORMAL)
            if line is None:
                self.delete(1.0, Tkinter.END)
            else:
                self.insert(Tkinter.END, line)
            self.see(Tkinter.END)
            self.update_idletasks()
            self.queue.task_done()
        self.config(state=Tkinter.DISABLED)
        self.after(100, self.update)
    
    def error(self, info):
        tkMessageBox.showerror("Error", info)
    
    def clear(self):
        self.queue.put(None)

class TkinterInterface(Tkinter.Tk):
    def __init__(self, callback, *args, **kwargs):
        Tkinter.Tk.__init__(self, *args, **kwargs)
        
        self.filename = Tkinter.StringVar()
        self.directory = False
        self.torrentfile = Tkinter.StringVar()
        self.mediafiles = []
        self.callback = callback
        
        self.title("PyTDMaker")
        
        self.announce_label = Tkinter.Label(self, text="Announce:")
        self.announce_box = Tkinter.Entry(self, width=30)
        
        self.button_file = Tkinter.Button(self, width=12, text='Open File', command=self._button_file)
        self.button_folder = Tkinter.Button(self, width=12, text='Open Folder', command=self._button_folder)
        self.root_label = Tkinter.Label(self, width=30, anchor=Tkinter.W, textvariable=self.filename)
        
        self.button_media = Tkinter.Button(self, width=12, text='Select Media Files', command=self._button_media)
        self.button_media.config(state=Tkinter.DISABLED)
        self.media_label = ThreadedText(self, height=5, setgrid=True, background='white')
        
        self.button_start = Tkinter.Button(self, width=12, text='Start', command=self._button_torrent)
        self.textbox = ThreadedText(self, height=20, setgrid=True, background='white')
        
        self.announce_label.grid(row=0, column=0, columnspan=1)
        self.announce_box.grid(row=0, column=1, columnspan=2)
        self.button_file.grid(row=0, column=3)
        self.button_folder.grid(row=0, column=4)
        self.root_label.grid(row=1, column=0, columnspan=3)
        
        self.button_media.grid(row=1, column=3, columnspan=1)
        
        self.button_start.grid(row=1, column=4, columnspan=1)
        self.textbox.grid(row=3, column=0, columnspan=5)
    
    def get_srcfile(self):
        return self.filename.get().strip()
    
    def get_torrentfile(self):
        return self.torrentfile.get().strip()
    
    def get_announce(self):
        return self.announce_box.get().strip()
    
    def get_mediafiles(self):
        return self.mediafiles
    
    def is_filled_out(self):
        if self.get_srcfile() == "" or self.get_torrentfile() == "" or self.get_announce() == "":
            return False
        elif self.directory and len(self.mediafiles) == 0:
            return False
        else:
            return True
    
    def put_info(self, info, stderr=False, clear=False):
        if stderr:
            self.textbox.error(info)
            return
        if clear:
            self.textbox.clear()
        self.textbox.write(info)
    
    def _button_folder(self):
        logging.debug("Open Folder button pressed.")
        self.filename.set(tkFileDialog.askdirectory(title='Media Directory'))
        if not self.filename.get():
            logging.debug("User did not select a folder.")
            return
        logging.info("User chose a folder: {0}".format(self.filename.get()))
        logging.debug("Showing media file input.")
        self.directory = True
        self.media_label.grid(row=2, column=0, columnspan=5)
        
        self.media_label.clear()
        self.media_label.write("Please select the main media files...")
        self.button_media.config(state=Tkinter.NORMAL)

    def _button_file(self):
        logging.debug("Open File button pressed.")
        self.filename.set(tkFileDialog.askopenfilename(multiple=False))
        if not self.filename.get():
            logging.debug("User did not select a file.")
            return
        logging.info("User chose a file: {0}".format(self.filename.get()))
        self.mediafiles = [self.filename.get()]
        logging.debug("Hiding media file input.")
        self.directory = False
        self.media_label.grid_remove()
        self.button_media.config(state=Tkinter.DISABLED)
    
    def _button_media(self):
        if not self.directory:
            logging.warning("Media button was somehow pressed even though it should be disabled.")
            return
        logging.debug("User pressed Add Media button.")
        self.mediafiles += [str(filename.strip("{}")) for filename in re.split("\} \{", tkFileDialog.askopenfilename(multiple=True, initialdir=self.filename))]
        logging.info("Currently selected media files:\n" + "\n".join(map(str, self.mediafiles)))
        
        self.media_label.clear()
        for file in self.mediafiles:
            self.media_label.write(str(file) + "\n")

    def _button_torrent(self):
        self.torrentfile.set(tkFileDialog.asksaveasfilename(filetypes=[('Torrent files', '.torrent')],
                                                            title='Where to save torrent file',
                                                            initialfile=self.filename.get() + '.torrent').replace("/", os.sep))
        logging.info("User chose torrent file path: {0}".format(self.torrentfile.get()))
        self.after(1000, self.callback())

class CLIInterface():
    def __init__(self, callback, *args):
        self.callback = callback
        parser = argparse.ArgumentParser(description="Create a description and torrent for a media files(s).", add_help=True)
        parser.add_argument("--version", action="version")
        parser.add_argument("-t", "--torrent", dest="torrentfile", help="Where to store the torrent file.", default=False)
        parser.add_argument("-o", "--output", help="Where to output to.", default=sys.stdout, type=open)
        parser.add_argument("-a", "--announce", help="Announce URL for your tracker.", default="")
        parser.add_argument("srcfile", nargs="?", metavar="input", help="File or folder to describe.", default="")
        parser.add_argument("mediafiles", nargs="*", metavar="media", help="Actual media files if input is a directory.", default=[])
        self.kwargs = vars(parser.parse_args(args))
        if self.kwargs["torrentfile"] == False:
            self.kwargs["torrentfile"] = os.path.basename(self.kwargs["srcfile"])
        if not os.path.isdir(self.kwargs["torrentfile"]):
            self.kwargs["mediafiles"] = [self.kwargs["srcfile"]]
    
    def mainloop(self):
        return self.callback()
    
    def get_srcfile(self):
        return self.kwargs['srcfile'] if 'srcfile' in self.kwargs.keys() else ""
    
    def get_torrentfile(self):
        return self.kwargs['torrentfile'] if 'torrentfile' in self.kwargs.keys() else ""
    
    def get_announce(self):
        return self.kwargs['announce'] if 'announce' in self.kwargs.keys() else ""
    
    def get_mediafiles(self):
        return self.kwargs['mediafiles'] if 'mediafiles' in self.kwargs.keys() else []
    
    def is_filled_out(self):
        if self.kwargs["srcfile"] == "":
            logging.info("No input file specified. We shouldn't have even gotten here.")
            return False
        elif self.kwargs["announce"] == "":
            logging.error("No announce specified." + str(self.kwargs["srcfile"]))
            return False
        elif not os.path.exists(self.kwargs["srcfile"]):
            logging.error("Input file does not exist.")
            return False
        elif os.path.isdir(self.kwargs["srcfile"]) and len(self.kwargs["mediafiles"]) == 0:
            logging.error("No media files given for a directory input.")
            return False
        elif not all(map(os.path.exists, self.kwargs["mediafiles"])) or any(map(os.path.isdir, self.kwargs["mediafiles"])):
            logging.error("One or more media files does not exist or is a directory.")
            return False
        else:
            logging.info("Valid CLI arguments. Entering CLI mode.")
            return True
    
    def put_info(self, info, stderr=False, clear=True):
        if stderr:
            sys.stderr.write("Error: " + info)
            return
        self.kwargs["output"].write(info)

class ThreadedProgram:
    def __init__(self, gui, *args, **kwargs):
        tmp_directory = tempfile.mkdtemp()
        os.chdir(tmp_directory)
        logging.debug("Outputting to temporary directory {0}.".format(tmp_directory))
    
        self.gui = gui(self, *args, **kwargs)
        self.queue = Queue.Queue()
        self.info = ""
        self.threads = []
        self.running = False
        self.num_pics = NUM_PICS
    
    def execute(self):
        self.gui.mainloop()
    
    def __del__(self):
        logging.debug("Removing temporary directory.")
        tmp_directory = os.getcwd()
        os.chdir("..")
        os.rmdir(tmp_directory)
    
    def __call__(self):
        announce = self.gui.get_announce()
        filename = self.gui.get_srcfile()
        torrentname = self.gui.get_torrentfile()
        logging.info("Announce Url: {0}".format(announce))
        if self.running:
            logging.warning("User tried to start a job while another was running.")
            self.gui.put_info('A job is already running!', True)
            return
        elif not self.gui.is_filled_out():
            logging.warning("User tried to start a job without filling in all the fields.")
            self.gui.put_info('You need to fill in all the fields!', True)
            return
        
        for filename in self.gui.get_mediafiles():
            self.queue.put(filename)
        
        size = os.path.getsize(filename)
        threads_total = self._get_cpu_count()
        # First calculate mktorrent threads based on ratio.
        mktorrent_threads = int(min(threads_total - 1, max(1, math.floor(threads_total * MKTORRENT_RATIO * (size / 1024 ** 3) / self.queue.qsize()))))
        # Make sure not to have more info threads than there are files.
        other_threads = min(self.queue.qsize(), threads_total - mktorrent_threads)
        # Recalculate mktorrent threads if the info thread number was too high.
        mktorrent_threads = mktorrent_threads = threads_total - other_threads
        
        logging.debug("Using {0} threads for mktorrent and {1} for info capture.".format(mktorrent_threads, other_threads))
        
        kwargs = {"announce": announce, "filename": filename, "torrentname": torrentname, "threads": mktorrent_threads}
        torrent_thread = threading.Thread(target=self._thread_torrent, kwargs=kwargs)
        torrent_thread.daemon = True
        self.threads.append(torrent_thread)
        for i in range(other_threads):
            t = threading.Thread(target=self._thread_info)
            t.daemon = True
            self.threads.append(t)
        
        logging.info("Starting worker threads.")
        finalthread = threading.Thread(target=self._thread_worker)
        finalthread.daemon = True
        self.running = True
        finalthread.start()
        map(operator.methodcaller('start'), self.threads)
        logging.debug("Finished starting worker threads.")
    
    def _thread_worker(self):
        logging.info("Thread watching thread initializing.")
        self.queue.join()
        map(operator.methodcaller('join'), self.threads)
        self.running = False
        self.gui.put_info(self.info, clear=True)
        self.threads = []
        logging.info("Description complete. Putting info into textbox.")
    
    def _thread_info(self):
        logging.debug("Info thread initializing.")
        while not self.queue.empty():
            filename = self.queue.get(True)
            fp = VideoFile(self, self.gui, filename)
            fp.make_mediainfo()
            fp.make_screenshots()
            self.info += str(fp)
            self.queue.task_done()
        logging.debug("Exiting thread.")
    
    def _thread_torrent(self, announce, filename, torrentname, threads=2):
        logging.debug("Torrent thread initializing.")
        fp = VideoFile(self, self.gui, filename)
        fp.make_torrent(announce, torrentname, threads)
        logging.debug("Exiting thread.")
    
    def _get_cpu_count(self):
        # Python 2.6+
        try:
            import multiprocessing
            return multiprocessing.cpu_count()
        except (ImportError, NotImplementedError):
            pass

        # POSIX
        try:
            res = int(os.sysconf('SC_NPROCESSORS_ONLN'))
            if res > 0:
                return res
        except (AttributeError, ValueError):
            pass

        # Windows
        try:
            res = int(os.environ['NUMBER_OF_PROCESSORS'])
            if res > 0:
                return res
        except (KeyError, ValueError):
            pass

        # jython
        try:
            from java.lang import Runtime
            runtime = Runtime.getRuntime()
            res = runtime.availableProcessors()
            if res > 0:
                return res
        except ImportError:
            pass

        # BSD
        try:
            sysctl = subprocess.Popen(['sysctl', '-n', 'hw.ncpu'], stdout=subprocess.PIPE)
            scStdout = sysctl.communicate()[0]
            res = int(scStdout)
            if res > 0:
                return res
        except (OSError, ValueError):
            pass

        # Linux
        try:
            res = open('/proc/cpuinfo').read().count('processor\t:')
            if res > 0:
                return res
        except IOError:
            pass

        # Solaris
        try:
            pseudoDevices = os.listdir('/devices/pseudo/')
            expr = re.compile('^cpuid@[0-9]+$')
            res = 0
            for pd in pseudoDevices:
                if expr.match(pd) != None:
                    res += 1

            if res > 0:
                return res
        except OSError:
            pass

        # Other UNIXes (heuristic)
        try:
            try:
                dmesg = open('/var/run/dmesg.boot').read()
            except IOError:
                dmesgProcess = subprocess.Popen(['dmesg'], stdout=subprocess.PIPE)
                dmesg = dmesgProcess.communicate()[0]
            res = 0
            while '\ncpu' + str(res) + ':' in dmesg:
                res += 1
            if res > 0:
                return res
        except OSError:
            pass

        # If unable to determine CPU count, default to 2.
        return 2

FORMAT = "%(asctime)s [%(thread)d] - %(levelname)s [%(funcName)s, %(lineno)d] : %(message)s"
logging.basicConfig(filename='pytdmaker.log', level=logging.DEBUG, format=FORMAT)
logging.info("Initializing program.")

gui = TkinterInterface if len(sys.argv) == 1 else CLIInterface

try:
    program = ThreadedProgram(gui, *sys.argv[1:])
    logging.info("Entering main loop.")
    program.execute()
except KeyboardInterrupt:
    logging.info("User pressed Ctrl-C.")
else:
    logging.info("Script complete. No critical errors.")
finally:
    logging.info("Closing program.")
